package com.howie.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.howie.gulimall.product.service.CategoryBrandRelationService;
import com.howie.gulimall.product.vo.gulimall_vo.Catelog2Vo;
import org.apache.commons.lang.StringUtils;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.howie.common.utils.PageUtils;
import com.howie.common.utils.Query;

import com.howie.gulimall.product.dao.CategoryDao;
import com.howie.gulimall.product.entity.CategoryEntity;
import com.howie.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    /* @Autowired
    CategoryDao categoryDao;
    使用细节，这里categoryDao等价于baseMapper */
    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }
    /*
     * 树形菜单的转换
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        // 1、查询所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);// 没有查询条件就是查询所有
        // 2、组装成父子的树形结构
        // 2.1、使用Stream过滤数据，找到所有的一级分类
        List<CategoryEntity> level1Menus = entities.stream().filter((categoryEntity) -> {
            return categoryEntity.getParentCid() == 0;
        }).map((menu) -> {
            // 2.2、使用递归方法，找到二级分类
            menu.setChildren(getChildren(menu,entities));
            return menu;
        }).sorted((menu1,menu2) -> {
            return (menu1.getSort() ==null ? 0:menu1.getSort())- (menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());

        return level1Menus;
    }
    /*
     * 删除没有绑定的菜单
     */
    @Override
    public void removeMenuByIds(List<Long> asList) {
        // TODO 1、检查当前删除的菜单，是否被别的地方引用
        // 逻辑删除
        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);

        // 父节点查找出来是逆序的，返回给前端时，我们应该以顺序返回，以便解析
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[0]);
    }
    /**
     * 级联更新所有关联的数据
     * 注解@CacheEvict：缓存"失效模式"的使用方式(只能清除一个缓存，清除多个缓存使用注解@Caching)
     * 注解@CachePut：缓存"双写模式"的使用方式(更新数据的同时放入缓存)
     * spel表达式：普通字符串一定要加上单引号
     * 1、同时进行多种缓存操作 @Caching
     * 2、指定删除某个分区下的所有缓存：@CacheEvict(value = {"category"},allEntries = true)
     *      批量删除category分区下的缓存，这种分区只是在Spring中有效，在redis中没有意义
     *
     * 小结：存储同一类的数据，都可以指定成同一个分区。分区名默认就是缓存的前缀。
     * SpringCache的不足：
     *  1、读模式
     *      缓存穿透：查询一个null数据。解决方案(允许缓存null)：spring.cache.cache-null-values: true
     *      缓存击穿：大并发请求同时查询一个缓存过期的数据，解决方案(加锁！)：SpringCache有没有呢？？默认是没有加锁
     *          需要手动开启：@Cacheable(value = {"category"},key = "#root.methodName",sync = true)，sync属性只有此注解拥有
     *      缓存雪崩：大量的key同时过期。解决方案(加随机时间+过期时间)
     *  2、写模式(保证缓存与数据库一致)
     *      ①读写加锁
     *      ②引入中间件Canal，感知到MySQL的更新去跟新缓存
     *      ③读多些多，直接去数据库查询
     *
     *  总结：
     *      常规数据(读多些少，即时性、一致性要求不高的数据)完全可以使用Spring-Cache，写模式(只要有过期时间就足够了)
     *      特殊数据需要特殊设计
     *  3、SpringCache原理：
     *      CacheManager(RedisCacheManager) -> Cache(RedisCache) -> Cache负责缓存的读写
     */
    /*
    同时进行多种缓存操作(删除缓存)：
    @Caching(evict = {
            @CacheEvict(value = {"category"}, key = "'getAllCategoryWithLevel1'"),
            @CacheEvict(value = {"category"}, key = "'catalog_json'")
    })*/
    @CacheEvict(value = {"category"},key = "'getAllCategoryWithLevel1'")
    @Transactional // 开启事务支持
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        if(StringUtils.isNotEmpty(category.getName())){
            // 同步更新其他关联表中的数据
            categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
            // TODO 更新其他关联
        }
    }
    /*
     注解@Cacheable：代表当前方法的结果需要缓存，如果缓存中有此方法的结果，就不调用方法，直接从缓存中返回此方法的结果，如果缓存中没有此方法
     的结果，就调用此方法返回结果并把结果写入缓存。
     注意点：每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。[缓存的分区(按照业务类型分)]

     默认行为：
        1、如果缓存中有，方法不用调用。
        2、key默认自动生成，缓存的名字 "catagory::SimpleKey []"(自主生成的key值)
        3、缓存的value值，默认使用jdk序列化机制，将序列化的数据存到redis
        4、默认过期时间 -1
     自定义：
        1、指定生成缓存的名字key，使用key属性指定接收一个SpEL(key = "#root.methodName")
        2、指定缓存的数据存活时间ttl(从配置文件里配置redis.time-to-live，单位为毫秒)
        3、将数据保存为json格式
     */
    @Override
    @Cacheable(value = {"category"},key = "#root.methodName",sync = true)
    public List<CategoryEntity> getAllCategoryWithLevel1() {
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>()
                .eq("parent_cid",0));
        return categoryEntities;
    }

    /**
     * 封装首页渲染二级三级分类数据时所需的json字符串(配合redis缓存中间件)
     */
    // TODO 产品上线久了可能会出现 堆外内存溢出：OutOfDirectMemoryError
    /*
    产生原因：
        1、SpringBoot 2.0 以后默认使用 lettuce作为操作 redis的客户端。它使用netty进行网络通信。
        2、lettuce的 bug 导致 netty 堆外内存溢出。-Xmx300m netty如果没有指定堆外内存，默认使用 -Xmx300m
        可以通过 -Dio.netty.maxDirectMemory 调大堆外内存。
        解决方案：不能使用 -Dio.netty.maxDirectMemory 只去调大堆外内存。治标不治本的行为。
            1、升级lettuce客户端
            2、切换使用jedis
     */
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson(){
        /*
        为提高系统的性能，我们加入缓存逻辑！并且规定缓存中存放的数据是json字符串。最后返回时还需要逆转为可用的Java对象(序列化与反序列化)。
        json字符串的好处：跨语言，跨平台兼容。
         */
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catalogJson = ops.get("catalog_json");
        if(StringUtils.isEmpty(catalogJson)){
            // 1、redis缓存中没有对应的分类数据，需要查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonFromDb= getCatalogJsonWithRedisLock(); // 存在缓存击穿问题，在此方法内加锁解决！
            // 2、查询到的数据放入redis缓存，将对象转为json放在缓存中
            // ops.set("catalog_json",JSON.toJSONString(catalogJsonFromDb),1, TimeUnit.DAYS); // 1天以后过期
            return catalogJsonFromDb;
        }
        // 逆转的类型比较复杂，所以使用 TypeReference
        return JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
    }

    /**
     * 从数据库查询并封装分类数据
     * 两种加锁方式(只要是同一把锁就能锁住需要这个锁的所有线程)：
     *   1、同步代码块，把所有方法体使用同步代码块包起来。synchronized(同步监视器){// 需要被同步的代码，即方法体}
     *      这里同步监视器可以使用this。因为SpringBoot所有组件在IOC容器中默认都是单例的。
     *      其中一个线程竞争到锁以后，应该再去缓存中确定一次，如果没有才需要继续查询。
     *      本地锁(synchronized this lock等)只能锁住当前应用下的线程，在分布式情况下，想要锁住所有，必须使用分布式锁。
     *   2、分布式锁(redisson)
     *
     */
    private Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String data = ops.get("catalog_json");
        // 再次确定缓存中是否有key为catalog_json的数据
        if(StringUtils.isNotEmpty(data)) {
            // 如果有数据，线程进来就不需要再查询数据库
            return JSON.parseObject(data,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        }
        // 1、查出所有一级分类
        List<CategoryEntity> categoryEntities = getAllCategoryWithLevel1();
        // 2、封装数据
        Map<String, List<Catelog2Vo>> dataMap = categoryEntities.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // 每一个一级分类的二级分类
            List<CategoryEntity> entities = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>()
                    .eq("parent_cid", v.getCatId()));
            // 封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (entities != null) {
                catelog2Vos = entities.stream().map(entity -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo();
                    catelog2Vo.setCatalog1Id(v.getCatId().toString());
                    catelog2Vo.setId(entity.getCatId().toString());
                    catelog2Vo.setName(entity.getName());
                    // 查找当前二级分类的三级分类信息，并把信息封装到(Catelog2Vo.Catelog3Vo)
                    List<CategoryEntity> categoryLevel3List = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>()
                            .eq("parent_cid", entity.getCatId()));
                    List<Catelog2Vo.Catelog3Vo> catelog3Vos = null;
                    if(categoryLevel3List != null){
                        catelog3Vos = categoryLevel3List.stream().map(level3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(entity.getCatId().toString(),
                                    level3.getCatId().toString(), level3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                    }
                    catelog2Vo.setCatalog3List(catelog3Vos);
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        // 查询到的数据放入redis缓存，将对象转为json放在缓存中
        ops.set("catalog_json",JSON.toJSONString(dataMap),1, TimeUnit.DAYS); // 1天以后过期
        return dataMap;
    }
    /**
     * 分布式锁：使用redis `占坑` 命令setnx。还要加上锁的过期时间，防止因宕机(无法及时释放锁)产生死锁问题！
     * 由于种种原因(停电等不确定因素)，还是可能使锁没有设置上过期时间导致死锁问题，所以我们必须保证给锁设置上了过期时间！！！
     * `占坑`和设置过期时间是原子操作(set lock lock EX 300 NX)，即可保证设置上了过期时间，也可以避免死锁问题。
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock() {
        // 1、去redis占分布式锁(setnx)
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String uuid = UUID.randomUUID().toString();
        Boolean isLock = ops.setIfAbsent("lock", uuid,300,TimeUnit.SECONDS); // 保证加锁与设置过期时间同步
        if(isLock) {
            // 占锁成功
            // 设置过期时间，必须和加锁是同步的，原子的。
            // stringRedisTemplate.expire("lock",30,TimeUnit.SECONDS); // 30过期
            Map<String, List<Catelog2Vo>> catalogJson = null;
            try {
                catalogJson = getCatalogJsonFromDb();
            } finally {
                // 解锁(释放锁)
                /*
                因为设置了过期时间，锁会自动释放！如果因为业务处理过慢，到了过期时间，锁会自动释放！此时如果执行delete，删除的就是别人的锁。
                所以一定要确认删除的锁是自己的锁！
                最终代码：获取值对比 + 对比成功删除 = 原子操作(lua 脚本解锁)
                 */
                /*String lockValue = ops.get("lock");
                if(uuid.equals(lockValue)){
                    stringRedisTemplate.delete("lock"); // 删除自己的锁
                }*/
                // lua 脚本(原子操作删除脚本)
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class), Collections.singletonList("lock"),uuid);
            }
            return catalogJson;
        }else {
            // 占锁失败，重试。就像synchronized()一样继续监听锁的释放！(自旋)
            try {
                Thread.sleep(100); // 休眠100ms后重试
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonWithRedisLock(); // 自旋的方式
        }
    }
    /**
     * 递归查找catelogId的父节点
     */
    private List<Long> findParentPath(Long catelogId,List<Long> paths){
        paths.add(catelogId);
        CategoryEntity entity = this.getById(catelogId);
        if(entity.getParentCid() != 0){
            findParentPath(entity.getParentCid(),paths);
        }
        return paths;
    }

    /**
     * 递归查找所有菜单的子菜单
     */
    private List<CategoryEntity> getChildren(CategoryEntity root,List<CategoryEntity> all){
        List<CategoryEntity> children = all.stream().filter((categoryEntity) -> {
            return categoryEntity.getParentCid().equals(root.getCatId());
        }).map((categoryEntity) -> {
            // 1、找子菜单
            categoryEntity.setChildren(getChildren(categoryEntity, all));
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            // 2、菜单的排序
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());
        return children;
    }
}